<?php

require_once 'VersionControl/Subversion/Adapter/Interface.php';

class Skjb_VersionControl_Subversion_Adapter_Svn implements Skjb_VersionControl_Subversion_Adapter_Interface
{
    protected $config = array(
        'host' => null,
        'port' => 3690,
        'path' => null,
        'username' => '',
        'password' => '',
    );
    
    private $_connection = null;
    
    private $_version = 'SVN/1.6.9';
    
    protected $realm = null;
    
    protected $uuid = null;
    
    protected $repositoryUrl = null;
    
    public function __construct($config = null)
    {
        if ($config !== null) {
            $this->setConfig($config);
        }
    }
    
    public function setConfig($config) {
        if ($config instanceof Zend_Config) {
            $config = $config->toArray();
        } elseif (!is_array($config)) {
            throw new Exception('Array or Zend_Config object expected, got ' . gettype($config));
        }

        foreach ($config as $k => $v) {
            $this->config[strtolower($k)] = $v;
        }

        return $this;
    }
    
    public function __destruct()
    {
        if ($this->_connection !== null) {
            fclose($this->_connection);
        }
    }
    
    private function _connect()
    {
        if (!$this->_connected) {
            $this->_connection = fsockopen($this->config['host'], $this->config['port']);
            
            if (false === $this->_connection) {
                throw new Exception('Unable to connect');
            }
            
            // Get the versions and capabilities
            $response = $this->_getResponse(); // ( success ( minversion maxversion ( ) ( capability ... ) ) )
            preg_match('/\( success \( (\d+) (\d+) \( \) \( (.+) \) \) \)/', $response, $matches);
            $minVersion = $matches[1];
            $maxVersion = $matches[2];
            //$serverCapabilities = explode(' ', $matches[3]);
            
            if ($minVersion > 2 || $maxVersion < 2) {
                throw new Exception('Unsupported server version');
            }
            
            // Send the capabilities list
            $fullpath = 'svn://' . $this->config['host'] . $this->config['path'];
            $this->_sendCommand('( 2 ( edit-pipeline svndiff1 absent-entries depth mergeinfo log-revprops ) ' . strlen($fullpath) . ':' . $fullpath . ' ' . strlen($this->_version) . ':' . $this->_version . ' ( ) ) ');
            
            // Get the auth type
            $response = $this->_getResponse(); // ( success ( ( authtype ) realm ) )
            preg_match('/\( success \( \( (.+) \) \d+:(.+) \) \)/', $response, $matches);
            $authType = $matches[1];
            $this->realm = $matches[2];
            
            switch ($authType) {
                case 'CRAM-MD5':
                    // Accept the auth type
                    $this->_sendCommand('( CRAM-MD5 ( ) ) ');
                    
                    // Get the auth token
                    $response = $this->_getResponse(); // ( step ( token ) )
                    preg_match('/\d\d:([^ ]+) /', $response, $matches);
                    $token = $matches[1];
                    
                    // Send the auth response
                    $hash = bin2hex(mhash(MHASH_MD5, $token, $this->config['password']));
                    $send = $this->config['username'] . ' ' . $hash;
                    $this->_sendCommand(strlen($send) . ':' . $send . ' ');
                    
                    // If the authentication failed, throw an exception
                    $response = $this->_getResponse();
                    if (preg_match('/\( failure \( \d+:(.+) \) \)/', $response, $matches)) {
                        echo 'Auth failure: ' . $matches[1];
                        throw new Exception('Auth failure: ' . $matches[1]);
                    }
                    break;
                default:
                    throw new Exception('Unsupported auth type');
            }
            
            // Get the server details
            $response = $this->_getResponse(); // ( success ( uuid repos-url ( capability ... ) ) )
            preg_match('/\( success \( \d+:(.+) \d+:(.+) \( (.+) \) \) \)/', $response, $matches);
            $this->uuid = $matches[1];
            $this->repositoryUrl = $matches[2];
            //$sharedCapabilities = explode(' ', $matches[3]);
            
            $this->_connected = true;
        }
    }
    
    public function getLatestRev()
    {
        $this->_connect();
        
        $this->_sendCommand('( get-latest-rev ( ) ) ');
        $this->_getResponse(); // auth request
        $response = $this->_getResponse(); // response
        
        preg_match('/(\d+)/', $response, $matches);
        $revision = $matches[1];
        
        return $revision;
    }
    
    /*public function getDatedRev($date)
    {
        $this->_connect();
        
        $this->_sendCommand('( get-dated-rev ( ' . strlen($date) . ':' . $date . ' ) ) ');
        $this->_getResponse(); // auth request
        $response = $this->_getResponse(); // response
        
        preg_match('/(\d+)/', $response, $matches);
        $revision = $matches[1];
        
        return $revision;
    }*/
    
    public function revPropList($revision)
    {
        $this->_connect();
        
        $this->_sendCommand('( rev-proplist ( ' . $revision . ' ) ) ');
        $response = $this->_getResponse(); // ( success ( ( ) 0: ) )
        
        $response = $this->_getResponse();
        preg_match_all('/\( \d+:svn:(.+) \d+:(.+) \)/U', $response, $matches);
        
        $properties = array();
        foreach ($matches[1] as $i => $property) {
            $properties[$property] = $matches[2][$i];
        }
        
        return $properties;
    }
    
    /*public function getDirectory($path = '', $revision = '')
    {
        $this->_connect();
        
        $this->_sendCommand('( get-dir ( ' . strlen($path) . ':' . $path . ' ( ' . $revision . ' ) false true ) ) ');
        $this->_getResponse(); // auth request
        $this->_getResponse(); // response
    }*/
    
    /*public function stat($path = '', $revision = '')
    {
        $this->_connect();
        
        $this->_sendCommand('( stat ( ' . strlen($path) . ':' . $path . ' ( ' . $revision . ' ) ) ) ');
        $this->_getResponse(); // auth request
        $this->_getResponse(); // response
    }*/
    
    private function _sendCommand($command)
    {
        fputs($this->_connection, $command);
    }
    
    private function _getResponse()
    {
        $open = 0;
        $result = '';
        
        // The end of line doesn't seem to ever be reached if you use fgets()
        // resulting in a timeout so we loop through the result one char at a time
        while (!feof($this->_connection)) {
            $char = fgetc($this->_connection);
            $result .= $char;
            if ($char == '(') {
                $open++;
            }
            if ($char == ')') {
                $open--;
                if ($open == 0) {
                    $char = fgetc($this->_connection); // Get the last char
                    $result .= $char;
                    break;
                }
            }
        }
        
        return $result;
    }
}